Day 17 到 21 講的擴充點,下一個問題一定是:能不能放進流程自動化,在 CI 或排程裡無人看守地跑?可以,但沒有人在場按核准的環境,會讓前面每一個設計各自冒出一個新的坑。今天講 claude -p:怎麼停、怎麼擋、怎麼判斷它到底有沒有成功。
claude -p 把 stdin 當資料、參數當指令,所以 git diff | claude -p "審查" 可以直接用。沒有資料可讀時,它會等 3 秒再警告,CI 裡要明確寫 < /dev/null。stdin 的上限是 10MB。輸出有三種格式:
| 格式 | 說明 |
|---|---|
text |
預設,純文字 |
json |
單一物件,含 result、subtype、is_error、num_turns、total_cost_usd、permission_denials;輪數或預算上限截斷時另有 errors[] |
stream-json |
逐行 JSON,必須加 --verbose |
加 --json-schema 會多出 structured_output 欄位,結論不必從自然語言猜,是 CI 最好解析的形式。成本欄位是客戶端估計值,不是帳單。-p 預設會把 session 寫進 ~/.claude/projects/ 並可用 --resume 續談,不想留紀錄就加 --no-session-persistence。
CI 的認證有優先序:雲端供應商憑證、ANTHROPIC_AUTH_TOKEN、ANTHROPIC_API_KEY,之後才是 OAuth。有一條容易踩:在 -p 裡,只要環境有 ANTHROPIC_API_KEY 就一定用它,即使已經登入訂閱帳號也會改計 API 用量。想用訂閱額度,要在本機執行 claude setup-token 產生一年期的 OAuth token,放進 CLAUDE_CODE_OAUTH_TOKEN,需要 Pro、Max、Team 或 Enterprise 方案,而且 --bare 不會讀它。
無人看守時還有兩個設定。重試預設 10 次,由 CLAUDE_CODE_MAX_RETRIES 控制;CLAUDE_CODE_RETRY_WATCHDOG=1 是專為這種環境設計的,會對 429 與 529 容量錯誤無限重試,但遇到回報額度用盡的 429 會立刻失敗。單次請求逾時預設 600000 毫秒,由 API_TIMEOUT_MS 調整。
| 情境 | 結果 |
|---|---|
--max-turns 1 截斷多輪任務 |
exit 1,subtype: error_max_turns,is_error: true,沒有 result 欄位,原因在 errors[] |
--max-budget-usd 0.001 |
exit 1,subtype: error_max_budget_usd,實際花了 0.0097 |
重點是預算不是硬上限:上限 0.001,實際花了將近十倍,所以它擋的是「之後不要再花」,不是「一分錢都不超過」。官方沒有給這兩種情況的退出碼數字,只說達到輪數上限會以錯誤結束。
預設權限下請它改一個沒核准的檔案,結果是:
exit 0 subtype: success is_error: false
permission_denials: [ { tool_name: "Edit", tool_input: { ... } } ]
被拒絕的動作只在 permission_denials 留一筆,退出碼是 0。PreToolUse hook 用 exit 2 擋下指令時也一樣:整體是 success、exit 0,被擋的呼叫出現在 permission_denials 裡。反過來,exit 1 也不只代表程式壞掉:
| 退出碼 | 情境 |
|---|---|
| 0 | 任務完成;權限被拒;hook 擋下;模型沒做成任何事 |
| 1 | 輪數用盡;預算用盡;無效旗標;model 不存在;--resume 找不到;無 prompt 也無 stdin |
只看退出碼的 CI,會把「被擋下」當成「通過」。所以守門腳本必須解析 JSON,至少看三個欄位:is_error、subtype 與 permission_denials。model 不存在這類 API 錯誤更要小心,它的 subtype 仍是 success,要看 is_error 與 terminal_reason。

沒有指定時,-p 的起始權限模式不固定:可能是 default,在第三方供應商或關閉遙測的環境、2.1.285 以上則是 auto。所以第一條原則是顯式指定 --permission-mode。官方給 CI 的建議是 dontAsk:所有會問的動作一律自動拒絕。bypassPermissions 只該出現在隔離的容器或 VM 裡,官方明說它擋不了 prompt injection;在這個模式下 deny 規則仍然有效,allow 規則則不起作用。沒人能答覆的場合還可以加 --permission-prompts none(2.1.259 起),需要核准的動作直接拒絕,並告訴模型不要重試。
deny 永遠贏過 allow,被 deny 的工具會直接從模型的工具清單消失。但有一個很容易誤以為擋住的情境:
claude -p "..." --permission-mode acceptEdits --disallowedTools Edit Write
工具清單裡確實沒有 Edit 與 Write,模型改用 Bash 執行 echo -n "X" > note.txt,沒有提示、沒有拒絕紀錄,檔案被改寫。這發生在 acceptEdits 下,它會自動核准工作目錄內的檔案操作;換成預設模式,同樣的重導會被拒。所以要禁寫,別只禁編輯工具,連 Bash 一起收,最可靠的是 --tools "Read" 白名單。注意 --allowedTools 只是「不問就能用」,要限制能力得用 --tools。

還有一條要記得:在從未信任過的資料夾,專案 .claude/settings.json 裡的 permissions.allow 在 -p 不會被採用,只會印出警告;deny 與 ask 規則不受影響,因為它們只會收緊。
hooks 會跑。 PostToolUse 照常觸發,PreToolUse 的 exit 2 能擋,被擋的那次不會觸發 PostToolUse。只有 exit 2 會擋,exit 1 不擋。
專案的 .mcp.json 不需核准就連線。 初始化事件顯示 status: connected、source: project,但呼叫它的工具仍需 allow,否則進 permission_denials;加上 --allowedTools "mcp__<server>__<tool>" 才會成功,--strict-mcp-config 則能把它排除。官方文件把這點講得更重:沒有 --bare 時,-p 會執行專案 settings 裡的 hooks、連線 .mcp.json 的 server,即使資料夾從未被信任,因為無頭模式沒有信任對話框,也沒有逐 server 核准。所以拿不可信的 repo 跑 CI,要用 --bare,或用 --setting-sources user 不讀專案設定,或用 disabledMcpjsonServers 擋名單。
載入範圍差很大。 同一個問題,--setting-sources project 載入約 31 個工具、19 個 skills,成本 0.0121;不隔離則是 57 個工具、345 個 skills、13 個 MCP server,成本 0.0834,約 7 倍,而且 --setting-sources project 關不掉已安裝的 plugin。--bare 只留 Bash、Edit、Read 三個工具,官方建議腳本與 SDK 呼叫都用它,並預告它未來會成為 -p 的預設,現在還不是。--bare 只吃 ANTHROPIC_API_KEY 或 apiKeyHelper,不讀 OAuth 登入。
用初始化事件看「這次載入了什麼」。 加上 --output-format stream-json --verbose,第一個 system/init 事件會列出實際載入的工具、MCP server、plugin、skills 與 permissionMode。其中 mcp_server_errors 在沒有錯誤時會省略,CI 可以直接對非空陣列判失敗,需要 2.1.219 以上,它記的是 --mcp-config 裡被設定驗證跳過的項目。
plan mode 會寫檔。 --permission-mode plan 不會改目標檔案,但模型會把計畫寫進 ~/.claude/plans/,位置可用 plansDirectory 調整。CI 對寫入位置有要求時要設定它,plan mode 不等於完全唯讀。
| 旗標 | 用途 |
|---|---|
--fallback-model sonnet,haiku |
主要模型過載時改用備援 |
--permission-prompt-tool |
指定一個 MCP 工具來回應權限提示,但它不能核准標記為需要使用者互動的工具 |
--init、--maintenance |
觸發 Setup hook,適合 CI 的一次性準備 |
--exclude-dynamic-system-prompt-sections |
搭配 -p 提高多使用者腳本的 prompt cache 命中 |
--include-hook-events |
在 stream-json 輸出裡帶出 hook 事件 |
另外兩個行為要知道:最終結果出來後,背景的 Bash 約 5 秒內被終止,背景 subagent 則會等到完成,預設閒置上限 10 分鐘;續談時回報的成本是整段對話的累計,包含先前每次執行。
把前面幾條組起來:唯讀審查 diff,限制預算與輪數,輸出結構化結果,用退出碼表達三種結局。
#!/usr/bin/env bash
SCHEMA='{"type":"object","properties":{"verdict":{"enum":["pass","fail"]},"issues":{"type":"array","items":{"type":"string"}}},"required":["verdict","issues"]}'
RAW=$(claude -p "Review the diff on stdin for security bugs and correctness bugs only. Do not modify anything." \
--model "${GATE_MODEL:-haiku}" --max-budget-usd 0.2 --max-turns 5 \
--tools "Read" --allowedTools "Read" \
--setting-sources project --no-session-persistence \
--output-format json --json-schema "$SCHEMA")
[ $? -ne 0 ] && { echo "gate error" >&2; exit 2; }
echo "$RAW" | python3 -c '
import sys, json
try:
j = json.load(sys.stdin)
if j.get("is_error") or j.get("subtype") != "success" or j.get("permission_denials"):
sys.exit(2)
verdict = j["structured_output"]["verdict"]
except Exception:
sys.exit(2)
sys.exit(0 if verdict == "pass" else 1)'
用法是 git diff origin/main... | ./ci-gate.sh。帶有 SQL 注入的 diff 回 exit 1 並列出問題,乾淨的 diff 回 exit 0,成本約 0.006 美元,換成不存在的 model 則回 exit 2。逐項看它為什麼這樣寫:--tools "Read" 讓模型根本看不到寫入與執行類工具,--allowedTools "Read" 讓讀取不再詢問;預算與輪數兩個上限讓它一定會停;--no-session-persistence 不在 CI 機器留下對話紀錄;--json-schema 讓結論有固定欄位。最關鍵的是第三個出口:守門本身壞掉、被權限擋下、沒有結構化結果,一律是 2,不能當成通過。
這支腳本用 --setting-sources project,被審查分支裡的 hooks 與 .mcp.json 會生效,只適合可信的分支;審查不可信的 PR 要改用 --bare 搭配 API key。

官方 Action 是 anthropics/claude-code-action@v1,有兩種自動偵測的模式:給了 prompt 輸入就是自動化模式,沒給就等 @claude 觸發詞。它沒有獨立的 allowed_tools、max_turns、model 輸入,這些都要放進 claude_args。認證用 ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN。一個唯讀審查的步驟長這樣:
permissions:
contents: read
pull-requests: read
issues: read
id-token: write # Action 預設的 GitHub App 認證需要
steps:
- uses: actions/checkout@v6
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: "Review this pull request for security issues"
claude_args: "--max-turns 5 --allowedTools Read"
這是步驟片段,外層還需要 on:、jobs: 與 runs-on。自動化模式的純文字結果只出現在 run log;只給 Read 時它沒有 GitHub 的工具,看不到 PR 的 diff 也無法留言,要留言得另外給 GitHub 的 MCP 工具。成本方面官方建議三件事:在 claude_args 設 --max-turns、設 workflow 層級的逾時、用 concurrency 限制並行。
安全上官方提醒得很具體:公開 repo 裡 fork 的 PR 拿不到 secrets,所以審查只會對同 repo 的分支跑;用 pull_request_target 或 workflow_run 時會帶 base repo 的 secrets,不要把不可信的 ref 檢出到工作目錄根;allowed_non_write_users 官方直說是顯著的安全風險;show_full_output 會把工具輸出公開到日誌,可能帶出 secrets。
Agent SDK 是同一套機制的函式庫版本,有 Python 與 TypeScript。幾個行為差異:沒設 systemPrompt 時,SDK 用的是只涵蓋工具呼叫的精簡 prompt,連安全指示也省略,與預設就用完整 Claude Code prompt 的 claude -p 不同;省略 settingSources 時,query() 會讀 user、project、local 三層設定、CLAUDE.md 與 .claude/ 底下的 skills 與 agents,要避免就傳空陣列,但全域的 ~/.claude.json 與 managed 政策設定不論如何都會被讀,官方因此明說不要拿預設選項做多租戶隔離;canUseTool 回呼對已被核准的工具不會觸發,所以不能靠它攔下已經 allow 的呼叫。
--permission-mode,不要靠預設;CI 優先 dontAsk,bypassPermissions 只進容器。--max-budget-usd 與 --max-turns,預算不是硬上限。--tools 白名單限制能力,不要只靠 --disallowedTools;禁寫就別留下 Bash。is_error、subtype 與 permission_denials,不要只看退出碼。--bare、--setting-sources user 或 disabledMcpjsonServers,別讓專案的 hooks 與 .mcp.json 直接生效。--no-session-persistence,stdin 沒資料就明確 < /dev/null。前面講的擴充點,很多都假設有人在旁邊按核准。無頭模式把那個人拿掉之後,剩下的就只有你事先寫下來的規則,而它沒有寫到的地方,模型會自己找出一條路。